package com.db.williamchart.renderer;

import com.db.williamchart.extensions.Pair;

import com.db.williamchart.ChartContract;
import com.db.williamchart.Painter;
import com.db.williamchart.animation.ChartAnimation;
import com.db.williamchart.data.DataPoint;
import com.db.williamchart.data.Frame;
import com.db.williamchart.data.Label;
import com.db.williamchart.data.Paddings;
import com.db.williamchart.data.configuration.ChartConfiguration;
import com.db.williamchart.data.configuration.LineChartConfiguration;
import com.db.williamchart.extensions.ExtensionUtils;
import com.db.williamchart.extensions.ListUtils;
import com.db.williamchart.renderer.executor.DebugWithLabelsFrame;
import com.db.williamchart.renderer.executor.DefineDataPointsClickableFrames;
import com.db.williamchart.renderer.executor.DefineHorizontalBarsClickableFrames;
import com.db.williamchart.renderer.executor.DefineVerticalTouchableFrames;
import com.db.williamchart.renderer.executor.MeasureBarChartPaddings;
import com.db.williamchart.renderer.executor.MeasureLineChartPaddings;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import kotlin.Triple;

public class LineChartRenderer implements ChartContract.Renderer {

    private ChartContract.LineView view;
    private Painter painter;
    private ChartAnimation<DataPoint> animation;

    private List<DataPoint> data = new ArrayList<>();
    private Frame outerFrame;
    private Frame innerFrame;
    private LineChartConfiguration chartConfiguration;
    private List<Label> xLabels;
    private List<Label> yLabels;

    public LineChartRenderer(ChartContract.LineView view,
                             Painter painter,
                             ChartAnimation<DataPoint> animation) {
        this.view = view;
        this.painter = painter;
        this.animation = animation;
    }

    @Override
    public boolean preDraw(ChartConfiguration configuration) throws IllegalAccessException {

        if (data.isEmpty()) {
            return true;
        }

        this.chartConfiguration = (LineChartConfiguration) configuration;

        if (chartConfiguration.getScale().notInitialized()) {
            chartConfiguration = chartConfiguration.copy(ListUtils.toScale(data));
        }

        xLabels = ListUtils.toLabels(data);
        float scaleStep = chartConfiguration.getScale().getSize() / RendererConstants.defaultScaleNumberOfSteps;
        yLabels = new ArrayList<>();
        for (int i = 0; i < RendererConstants.defaultScaleNumberOfSteps + 1; i++) {
            float scaleValue = chartConfiguration.getScale().getMin() + scaleStep * i;
            chartConfiguration.setLabelsFormatter(scaleValue);
            Label label = new Label(
                    chartConfiguration.getLabelsFormatter(),
                    0,
                    0
            );
            yLabels.add(label);
        }

        Float longestChartLabelWidth = ExtensionUtils.maxValueBy(yLabels, new ExtensionUtils.Selector<Label, Float>() {
            @Override
            public Float doSelector(Label value) {
                return painter.measureLabelWidth(
                        value.getLabel(),
                        chartConfiguration.getLabelsSize()
                );
            }
        });

        if (longestChartLabelWidth == null) {
            throw new IllegalArgumentException("Looks like there's no labels to find the longest width.");
        }

        Paddings paddings = new MeasureLineChartPaddings().invoke(
                chartConfiguration.getAxis(),
                painter.measureLabelHeight(chartConfiguration.getLabelsSize()),
                longestChartLabelWidth,
                RendererConstants.labelsPaddingToInnerChart,
                chartConfiguration.getLineThickness(),
                chartConfiguration.getPointsDrawableWidth(),
                chartConfiguration.getPointsDrawableHeight()
        );

        outerFrame = chartConfiguration.toOuterFrame();
        innerFrame = outerFrame.withPaddings(paddings);

        placeLabelsX(innerFrame);
        placeLabelsY(innerFrame);
        placeDataPoints(innerFrame);

        animation.animateFrom(innerFrame.getBottom(), data, new ChartAnimation.AnimateFromCallback() {
            @Override
            public void callback() {
                view.postInvalidate();
            }
        });

        return false;
    }

    @Override
    public void draw() {
        if (data.isEmpty()) {
            return;
        }

        if (chartConfiguration.getAxis().shouldDisplayAxisX()) {
            view.drawLabels(xLabels);
        }

        if (chartConfiguration.getAxis().shouldDisplayAxisY()) {
            view.drawLabels(yLabels);
        }

        List<Float> xLabelsPositions = new ArrayList<>();
        List<Float> yLabelsPositions = new ArrayList<>();

        for (Label label : xLabels) {
            xLabelsPositions.add(label.getScreenPositionY());
        }
        for (Label label : yLabels) {
            yLabelsPositions.add(label.getScreenPositionY());
        }

        view.drawGrid(
                innerFrame,
                xLabelsPositions,
                yLabelsPositions
        );

        if (chartConfiguration.getFillColor() != 0 || !chartConfiguration.getGradientFillColors().isEmpty()) {
            view.drawLineBackground(innerFrame, data);
        }

        view.drawLine(data);

        if (chartConfiguration.getPointsDrawableWidth() != -1) {
            view.drawPoints(data);
        }

        if (RendererConstants.inDebug) {
            List<Frame> frameList = Arrays.asList(outerFrame, innerFrame);

            List<Pair<Float, Float>> pairs = new ArrayList<>();
            for (DataPoint dataPoint : data) {
                pairs.add(new Pair<Float, Float>(dataPoint.getScreenPositionX(), dataPoint.getScreenPositionY()));
            }

            frameList.addAll(new DebugWithLabelsFrame().invoke(
                    painter,
                    chartConfiguration.getAxis(),
                    xLabels,
                    yLabels,
                    chartConfiguration.getLabelsSize()
            ));
            frameList.addAll(new DefineDataPointsClickableFrames().invoke(
                    innerFrame,
                    pairs,
                    chartConfiguration.getClickableRadius()
            ));
            view.drawDebugFrame(frameList);
        }
    }

    @Override
    public void render(List<Pair<String, Float>> entries) {
        data = ListUtils.toDataPoints(entries);
        view.postInvalidate();
    }

    @Override
    public void anim(List<Pair<String, Float>> entries, ChartAnimation<DataPoint> animation) {
        data = ListUtils.toDataPoints(entries);
        this.animation = animation;
        view.postInvalidate();
    }

    @Override
    public Triple<Integer, Float, Float> processClick(Float x, Float y) {
        if (x == null || y == null || data.isEmpty())
            return new Triple(-1, -1f, -1f);

        List<Pair<Float, Float>> pairList = new ArrayList<>();
        for (DataPoint point : data) {
            Pair<Float, Float> pair = new Pair<>(point.getScreenPositionX(), point.getScreenPositionY());
            pairList.add(pair);
        }
        List<Frame> frames = new DefineDataPointsClickableFrames().invoke(
                innerFrame,
                pairList,
                chartConfiguration.getClickableRadius()
        );
        int index = -1;
        for (int i = 0; i < frames.size(); i++) {
            Frame frame = frames.get(i);
            if (frame.contains(x, y)) {
                index = i;
                break;
            }
        }

        if (index != -1) {
            return new Triple<>(index,
                    data.get(index).getScreenPositionX(),
                    data.get(index).getScreenPositionY());
        } else {
            return new Triple<>(-1, -1f, -1f);
        }
    }

    @Override
    public Triple<Integer, Float, Float> processTouch(Float x, Float y) {
        if (x == null || y == null || data.isEmpty())
            return new Triple(-1, -1f, -1f);

        List<Pair<Float, Float>> pairList = new ArrayList<>();
        for (DataPoint point : data) {
            Pair<Float, Float> pair = new Pair<>(point.getScreenPositionX(), point.getScreenPositionY());
            pairList.add(pair);
        }
        List<Frame> frames = new DefineVerticalTouchableFrames().invoke(
                innerFrame,
                pairList
        );
        int index = -1;
        for (int i = 0; i < frames.size(); i++) {
            Frame frame = frames.get(i);
            if (frame.contains(x, y)) {
                index = i;
                break;
            }
        }

        if (index != -1) {
            return new Triple<>(index,
                    data.get(index).getScreenPositionX(),
                    data.get(index).getScreenPositionY());
        } else {
            return new Triple<>(-1, -1f, -1f);
        }
    }

    private void placeLabelsX(Frame innerFrame) {
        float labelsLeftPosition =
                innerFrame.getLeft() +
                        painter.measureLabelWidth(xLabels.get(0).getLabel(), chartConfiguration.getLabelsSize()) / 2;
        float labelsRightPosition =
                innerFrame.getRight() -
                        painter.measureLabelWidth(xLabels.get(xLabels.size() - 1).getLabel(), chartConfiguration.getLabelsSize()) / 2;
        float widthBetweenLabels = (labelsRightPosition - labelsLeftPosition) / (xLabels.size() - 1);
        float xLabelsVerticalPosition =
                innerFrame.getBottom() -
                        painter.measureLabelAscent(chartConfiguration.getLabelsSize()) +
                        RendererConstants.labelsPaddingToInnerChart;
        for (int i = 0; i < xLabels.size(); i++) {
            Label label = xLabels.get(i);
            label.setScreenPositionX(labelsLeftPosition + (widthBetweenLabels * i));
            label.setScreenPositionY(xLabelsVerticalPosition);
        }
    }

    private void placeLabelsY(Frame innerFrame) {
        float heightBetweenLabels =
                (innerFrame.getBottom() - innerFrame.getTop()) / RendererConstants.defaultScaleNumberOfSteps;
        float labelsBottomPosition =
                innerFrame.getBottom() + painter.measureLabelHeight(chartConfiguration.getLabelsSize()) / 2;

        for (int i = 0; i < yLabels.size(); i++) {
            Label label = yLabels.get(i);
            label.setScreenPositionX(
                    innerFrame.getLeft() -
                            RendererConstants.labelsPaddingToInnerChart -
                            painter.measureLabelWidth(label.getLabel(), chartConfiguration.getLabelsSize()) / 2
            );
            label.setScreenPositionY(labelsBottomPosition - heightBetweenLabels * i);
        }
    }

    private void placeDataPoints(Frame innerFrame) {
        float scaleSize = chartConfiguration.getScale().getSize();
        float chartHeight = innerFrame.getBottom() - innerFrame.getTop();
        float widthBetweenLabels = (innerFrame.getRight() - innerFrame.getLeft()) / (xLabels.size() - 1);

        for (int i = 0; i < data.size(); i++) {
            DataPoint dataPoint = data.get(i);
            dataPoint.setScreenPositionX(innerFrame.getLeft() + (widthBetweenLabels * i));
            dataPoint.setScreenPositionY(
                    innerFrame.getBottom() -
                            (chartHeight * (dataPoint.getValue() - chartConfiguration.getScale().getMin()) / scaleSize)
            );
        }
    }
}
